从源码看Flutter BuildContext的秘密
点击上方蓝字关注我,知识会给你力量
我们每次在写Flutter代码的时候,都会看到这个参数——BuildContext,在Android开发中,也经常看见一个类似的东西——Context,它们是不是一样的呢?其实说一样也对,它们都是上下文的关键承载者,但是却也不一样,因为它们本质上是两个不同的概念。在Flutter中,BuildContext的源码如下。
从注释中我们就可以看出,[BuildContext]对象实际上是[Element]对象。[BuildContext]接口是用来阻止对[Element]对象的直接操作,它就是为了避免直接操纵Element类而创建的。
❝Element是Flutter UI中的一个非常重要的组成,Flutter UI在创建时,会通过Widget的createElement方法创建Element,然后Framework会调用Element实例的mount方法,在这个方法中,根据需要创建RenderObject,并挂载到Element的renderObject属性上,实际的布局和绘制,通常都是通过RenderObject的实现类RenderBox来实现的。
❞
所以,我们甚至可以直接把Context强转为Element,从而调用Element的方法,例如下面的代码。
var size = ((context as Element).findRenderObject() as RenderBox).size;
(context as Element).markNeedsBuild();
在使用BuildContext的时候,我们最常见的一个误区就是下面这个例子。
这段代码很简单,就是在当前页面上路由到一个新的页面,但是我们执行后,上面的代码会报错。
从错误原因上我们可以看到,就是Context的问题,也就是of(context)这个方法。类似的代码风格,我们在Flutter中可以找到很多,例如下面这些。
Navigator.of(context)
Scaffold.of(context).openDrawer()
Theme.of(context).copyWith()
……
就以Navigator.of(context)为例,我们来看下of方法的实现。
可以看到,关键代码就是通过context.findRootAncestorStateOfType
而我们的Navigator的push操作就是通过找到的NavigatorState来完成的。
那么我们现在来看下上面的那个错误具体是怎么产生的。当我们在build函数中使用Navigator.of(context)的时候,这个context实际上是通过MyApp这个widget创建出来的Element对象,而of方法向上寻找祖先节点的时候(MyApp的祖先节点),其实并不存在MaterialApp,也就没有它所提供的Navigator,所以就出错了。
那么当我们把Scaffold的部分拆成另外一个widget的时候,我们在FirstPage的build函数中,获取的就是FirstPage的BuildContext,然后向上寻找发现了MaterialApp,并找到它提供的Navigator,于是就可以愉快进行页面跳转了。所以要解决这个问题,一般有两个方法,一个就是抽取出一个新的Widget组件,或者是通过Builder组件,为后续Widget创建一个新的BuildContext环境。
所以,看到这里,你可以认为,BuildContext,实际上就是当前Widget在Element树上的句柄。
下面我们就从源码角度,来看下BuildContext的创建与加载的过程。
我们以StatelessWidget为例,在创建StatelessWidget的时候,首先会去createElement,并将当前widget传给Element,即StatelessElement。
在这个Element中,我们发现它的build方法,实际上就是调用了Widget的build方法,同时,传入了this,这个this,实际上就是我们在Widget的build方法中看到的BuildContext,到处,我们终于理清了,为什么BuildContext就是Element了。
同时,正是由于Flutter视图的树形结构,可以让我们很方便的在树上游走,这就需要我们用到它的一些游走的方法。
dependOnInheritedElement | InheritedWidget | working on the base of ancestor's widget and rebuilding when ancestors change |
---|---|---|
dependOnInheritedWidgetOfExactType | T? | runtime type of widgets or functions or model |
describeElement | DiagnosticsNode | descriptions of elements |
describeMissingAncestor | List | List of missing ancestors |
describeOwnershipChain | DiagnosticsNode | describes the ownership chain to the error report |
describeWidget | DiagnosticsNode | describe the details and working features of the widget |
dispatchNotification | void | bubble notification indicator at the context |
findAncestorRenderObjectOfType | T? | runtime type of RenderObjectOfType |
findAncestorStateOfType | T? | runtime type of StateOfType |
findAncestorWidgetOfExactType | T? | runtime type of WidgetOfExactType |
findRenderObject | T? | current render object which is created by itSelf |
findRootAncestorStateOfType | T? | runtime type ancestor's of given T type stateful widgets instance |
getElementForInheritedWidgetOfExactType | InheritedElement? | Type of concrete inheritedWidget subclass |
noSuchMethod | dynamic | dynamic type of method |
toString | string | converting the date to string through .toString methods provided by the framework |
visitAncestorElements | void | works for the call back return function, call back can not be null |
visitChildElements | void | children of the widgets or visitor |
这些方法也不用死记硬背,你只需要时刻记得「那几棵树」即可。
向大家推荐下我的网站 https://www.yuque.com/xuyisheng 点击原文一键直达
专注 Android-Kotlin-Flutter 欢迎大家访问
往期推荐
更文不易,点个“三连”支持一下👇